/*
 *  Copyright 2005 Blandware (http://www.blandware.com)
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package com.blandware.atleap.webapp.taglib.core.grid;

import com.blandware.atleap.webapp.taglib.core.grid.util.Grid;
import com.blandware.atleap.webapp.taglib.core.grid.util.SortField;
import com.blandware.atleap.webapp.taglib.core.grid.util.EntityFieldComparator;
import com.blandware.atleap.common.util.PartialCollection;
import com.blandware.atleap.common.util.QueryInfo;

import javax.servlet.jsp.tagext.SimpleTagSupport;
import javax.servlet.jsp.JspTagException;
import javax.servlet.jsp.PageContext;
import java.util.*;
import java.lang.reflect.Method;

import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.context.ApplicationContext;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * <p>Contains common attributes and methods for tags which fetch rows.</p>
 * <p><a href="FetchRowsSupport.java.html"><i>View source</i></a></p>
 *
 * @author Sergey Zubtsovskiy <a href="mailto:sergey.zubtsovskiy@blandware.com">&lt;sergey.zubtsovskiy@blandware.com&gt;</a>
 * @author Roman Puchkovskiy <a href="mailto:roman.puchkovskiy@blandware.com">
 *         &lt;roman.puchkovskiy@blandware.com&gt;</a>
 * @version $Revision: 1.3 $ $Date: 2006/08/03 10:08:18 $
 */
public abstract class FetchRowsSupport extends SimpleTagSupport {

    protected transient final Log log = LogFactory.getLog(FetchRowsSupport.class);
    protected transient ApplicationContext applicationCtx = null;
    /**
	 * Identifier of Spring bean to use for getting rows for this grid
	 */
	protected String beanId;
    /**
	 * Method of specified bean to call to get collection for fetching.
	 * This method must return instance of <code>com.blandware.atleap.common.util.PartialCollection</code> and
	 * accept instance of <code>com.blandware.atleap.common.util.QueryInfo</code> as a single argument.
	 *
	 * @see com.blandware.atleap.common.util.PartialCollection
	 * @see com.blandware.atleap.common.util.QueryInfo
	 */
	protected String method;
    /**
	 * Collection of objects to display if beanId not specified
	 * Only instances of <cody>java.util.Collection</code>, </code>java.lang.Object[]</code> and <code>java.util.Map</code> are supported
	 */
	protected Object collection;
    /**
	 * Identifier for this iterator tag. It must be unique within one grid.
	 */
	protected String tagId;

    /**
     * Returns bean ID
     *
	 * @return bean ID
     * @see #beanId
	 * @jsp.attribute required="false"
	 * rtexprvalue="true"
	 * type="java.lang.String"
	 * description="Identifier of Spring bean to use for getting rows for this grid"
	 */
	public String getBeanId() {
		return beanId;
	}

    /**
     * Sets bean ID
     *
     * @param beanId bean ID to set
     * @see #beanId
     */
	public void setBeanId(String beanId) {
		this.beanId = beanId;
	}

    /**
     * Returns method used to obtain values from bean
     *
	 * @return method name
     * @see #method
	 * @jsp.attribute required="false"
	 * rtexprvalue="true"
	 * type="java.lang.String"
	 * description="Method of specified manager to call to get collection for fetching"
	 */
	public String getMethod() {
		return method;
	}

    /**
     * Sets method used to obtain values from bean
     *
     * @param method method name to set
     * @see #method
     */
	public void setMethod(String method) {
		this.method = method;
	}

    /**
     * Returns Collection to iterate over
     *
	 * @return collection
     * @see #collection
	 * @jsp.attribute required="false"
	 * rtexprvalue="true"
	 * type="java.lang.Object"
	 * description="Collection of objects to display if beanId not specified"
	 */
	public Object getCollection() {
		return collection;
	}

    /**
     * Sets Collection to iterate over
     *
     * @param collection collection to set
     * @see #collection
     */
	public void setCollection(Object collection) {
		this.collection = collection;
	}

    /**
     * Returns ID of tag
     *
	 * @return tag ID
     * @see #tagId
	 * @jsp.attribute required="false"
	 * rtexprvalue="true"
	 * type="java.lang.String"
	 * description="Identifier for this iterator tag. It must be unique within one grid."
	 */
	public String getTagId() {
		return tagId;
	}

    /**
     * Sets ID of tag
     *
     * @param tagId tag ID to set
     * @see #tagId
     */
	public void setTagId(String tagId) {
		this.tagId = tagId;
	}

    /**
     * Returns a parent grid tag. If no such tag can be found, exception is
     * thrown.
     *
     * @return parent grid tag
     * @throws javax.servlet.jsp.JspTagException thrown if no parent grid tag can be found
     */
    protected GridTag getParentGridTag() throws JspTagException {
        GridTag parentGridTag = (GridTag) findAncestorWithClass(this,
                                                                GridTag.class);

        if ( parentGridTag == null ) {
            JspTagException e = new JspTagException("Parent tag is invalid! "
                    + "This tag is only valid when nested within 'grid' tag");
            throw e;
        }
        return parentGridTag;
    }

    /**
     * Obtains a collection and returns it. Before it, parameters are checked;
     * if something is wrong, exception is thrown.
     *
     * @param parentGridTag grid tag to which this one belongs
     * @param pageContext page context
     * @param increaseRowCountInGrid whether to add number of rows in fetched
     * collection to number of rows in grid
     * @return fetched collection
     * @throws javax.servlet.jsp.JspTagException thrown if parameters are incorrect or some error
     * occure during fetching
     */
    protected Collection fetchCollection(GridTag parentGridTag,
                                         PageContext pageContext,
                                         boolean increaseRowCountInGrid)
            throws JspTagException {
        // check beanId, method and collection attributes
        if ( (beanId == null || method == null) && collection == null ) {
            // error - either collection or beanId with method attributes must
            // be specified
            String errorMessage = "Either collection or beanId with method attributes must be specified";
            JspTagException e = new JspTagException(errorMessage);
            throw e;
        }

        // Get parameters from parent Grid tag
        Integer pageSize = parentGridTag.getPageSize();

        Grid grid = parentGridTag.getGrid();

        Integer pageNumber = parentGridTag.getCurrentPageNumber();

        // check, if there are empty rows
        int gridTotal = parentGridTag.getGrid().getTotal().intValue();
        int pn = pageNumber.intValue();
        int ps = pageSize.intValue();

        // number of already displayed elements
        int displayedCount = ps * (pn - 1);

        Integer offset = new Integer(displayedCount > gridTotal ? displayedCount - gridTotal : 0);
        Integer limit = new Integer(displayedCount > gridTotal ? ps : displayedCount + ps - gridTotal);

        if ( limit.intValue() < 0 ) {
            limit = new Integer(0);
        }

        Collection result = null;
        if ( beanId == null || method == null ) {
            Collection c = null;
            // collection attribute is specified.
            if ( collection instanceof Collection ) {
                c = (Collection) collection;
            } else if ( collection instanceof Object[] ) {
                c = Arrays.asList((Object[]) collection);
            } else if ( collection instanceof Map ) {
                c = ((Map) collection).entrySet();
            } else {
                JspTagException e = new JspTagException("Only instances of java.util.Collection, java.lang.Object[] and java.util.Map are supported in collection attribute of RowsIteratorTag");
                throw e;
            }

            // sort collection
            if ( !grid.getSortFields().isEmpty() ) {
                SortField sortField = grid.getSortFieldByIndex(0, tagId);
                if ( sortField != null ) {
                    String property = sortField.getProperty();
                    EntityFieldComparator comparator = new EntityFieldComparator(property);
                    if ( !sortField.isOrderAscending() ) {
                        comparator.reverseOrder();
                    }
                    if ( !(collection instanceof Map) ) {
                        List list = new ArrayList(c);
                        Collections.sort(list, comparator);
                        c = list;
                    } else {
                        SortedMap sortedMap = new TreeMap(comparator);
                        sortedMap.putAll((Map) collection);
                        c = new LinkedHashSet(sortedMap.entrySet());
                    }
                }
            }

            List subList = new ArrayList();

            if ( c instanceof PartialCollection ) {
                PartialCollection partialCollection = (PartialCollection) c;
                if (increaseRowCountInGrid) {
                    grid.addTotal(partialCollection.getTotal());
                }
                subList = partialCollection.asList();
            } else {
                if (increaseRowCountInGrid) {
                    grid.addTotal(c.size());
                }
                List list = new LinkedList(c);

                if ( gridTotal + list.size() <= displayedCount ) {
                    subList = new ArrayList();
                } else {
                    int fromIndex = offset.intValue();
                    int toIndex = offset.intValue() + limit.intValue() < list.size() ? offset.intValue() + limit.intValue() : list.size();

                    if ( fromIndex < list.size() ) {
                        subList = list.subList(fromIndex, toIndex);
                    }
                }
            }
            result = subList;
        } else {
            PartialCollection rows = new PartialCollection();

            // Find Spring bean in application context and find specified method on this bean

            if ( applicationCtx == null ) {
                applicationCtx = WebApplicationContextUtils.getRequiredWebApplicationContext(pageContext.getServletContext());
            }

            Object bean = applicationCtx.getBean(beanId);
            if ( bean == null ) {
                String errorMessage = "No bean with ID=" + beanId + " could be found in application context";
                if ( log.isErrorEnabled() ) {
                    log.error(errorMessage);
                }
                JspTagException e = new JspTagException(errorMessage);
                throw e;
            }
            if ( log.isDebugEnabled() ) {
                log.debug("Bean with ID=" + beanId + " has been succefully found in application context");
            }

            Method method = null;
            try {
                method = bean.getClass().getMethod(this.method, new Class[]{QueryInfo.class});
            } catch ( NoSuchMethodException e ) {
                if ( log.isErrorEnabled() ) {
                    log.error(e.getMessage());
                }
                throw new JspTagException(e);
            }

            if ( log.isDebugEnabled() ) {
                log.debug("Method '" + this.method + "' was found on bean with ID=" + beanId);
            }

            // Invoke method in bean and get collection of elements to display. Eval body then
            try {
                String whereClause = grid.getWhereClause(tagId);
                String orderByClause = grid.getOrderByClause(tagId);
                QueryInfo queryInfo = new QueryInfo(whereClause, orderByClause, limit, offset);
                queryInfo.setQueryParameters(grid.getQueryParameters());
                rows = (PartialCollection) method.invoke(bean, new Object[]{queryInfo});

                if (increaseRowCountInGrid) {
                    parentGridTag.getGrid().addTotal(rows.getTotal().intValue());
                }

                // TODO: pay attention to this part of code
//				if ( gridTotal + rows.size() <= displayedCount ) {
//					rows = new PartialCollection();
//				}

            } catch ( Exception e ) {
                throw new JspTagException(e);
            }
            result = rows;
        }
        return result;
    }
}
